Multimédia TP1 - Compressão de Imagem¶

David Leitão [2019223148]

Rodrigo Machado [2019218299]

Rui Costa [2019224237]

In [1]:
    # Notebook setup
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

from report import *

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

plt.rcParams['figure.figsize'] = [5, 5]

barn = np.array(Image.open("imagens/barn_mountains.bmp"))
peppers = np.array(Image.open("imagens/peppers.bmp"))
logo = np.array(Image.open("imagens/logo.bmp"))

1. Compressão JPEG usando GIMP¶

1.1. barn_mountains.bmp¶

In [2]:
path = "ex1/barn_mountains"
tmp = np.array(Image.open(f"{path}/high.jpg"))
plt.title("barn_mountains - High (qf=75)")
viewImage(tmp)
2022-03-25T20:43:50.727410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Quando vista sem qualquer nível de ampliação, não existem grandes alterações visíveis. No entanto, podemos observar alguns artefactos nos contornos das figuras nas imagens.


In [3]:
tmp = np.array(Image.open(f"{path}/medium.jpg"))
plt.title("barn_mountains - Medium (qf=50)")
viewImage(tmp)
2022-03-25T20:43:50.835411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Já são observáveis vários artefactos, maioritariamente nos contornos. Podemos observar também nas sombras do celeiro e na base da montanha. Com alguma ampliação, podemos observar a divisão em píxeis.


In [4]:
tmp = np.array(Image.open(f"{path}/low.jpg"))
plt.title("barn_mountains - Low (qf=25)")
viewImage(tmp)
2022-03-25T20:43:50.929413 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Observam-se artefactos por toda a imagem. Podemos ver também no céu uma divisão clara em píxeis, que se torna ainda mais visível quanto mais se amplie a imagem.

1.2. peppers.bmp¶

In [5]:
path = "ex1/peppers"
tmp = np.array(Image.open(f"{path}/high.jpg"))
plt.title("peppers - High (qf=75)")
viewImage(tmp)
2022-03-25T20:43:51.013414 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

A imagem apresenta uma espécie de efeito granular e um brilho menos intenso. Podemos observar com bastante ampliação alguns artefactos nas cortinas.


In [6]:
tmp = np.array(Image.open(f"{path}/medium.jpg"))
plt.title("peppers - Medium (qf=50)")
viewImage(tmp)
2022-03-25T20:43:51.123412 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Já podemos observar na cortina uma maior quantidade de artefactos, tal como nos contornos das várias figuras. À semelhança da primeira imagem, com alguma ampliação já podemos observar uma divisão em píxeis nas cortinas.


In [7]:
tmp = np.array(Image.open(f"{path}/low.jpg"))
plt.title("peppers - Low (qf=25)")
viewImage(tmp)
2022-03-25T20:43:51.232410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

A imagem inteira apresenta artefactos visíveis por toda a imagem, especialmente por toda a cortina. A divisão em píxeis torna-se bastante evidente nos objetos vermelhos e nas zonas com reflexos de luz.

1.3. logo.bmp¶

In [8]:
path = "ex1/logo"
tmp = np.array(Image.open(f"{path}/high.jpg"))
plt.title("logo - High (qf=75)")
viewImage(tmp)
2022-03-25T20:43:51.316411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

À semlhança da primeira imagem não se observam grandes alterações visíveis sem ampliação. No entanto, quando aplicada alguma podemos ver mais uma vez artefactos nas zonas de contornos das duas partes do logo.


In [9]:
tmp = np.array(Image.open(f"{path}/medium.jpg"))
plt.title("logo - Medium (qf=50)")
viewImage(tmp)
2022-03-25T20:43:51.379411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Na parte vermelha do logo podemos observar artefactos na circunferência sem qualquer ampliação, tal como no fundo branco junto dos contornos. Aplicando alguma ampliação já conseguimos ver o mesmo no contorno da parte azul e podemos verificar também divisões em píxeis nas zonas de mudança de cor.


In [10]:
tmp = np.array(Image.open(f"{path}/low.jpg"))
plt.title("logo - Low (qf=25)")
viewImage(tmp)
2022-03-25T20:43:51.442411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Observam-se artefactos na parte colorida e no fundo branco, continuando a ser mais visível nos contornos do que no preenchimento. A pixelização nas zonas de mudança de cor torna se ainda mais percetível com uma menor ampliação do que na qualidade média. Podemos ainda observar alguma pixelização na zona de preênchimento da parte vermelha.

2. Comparação de Y com RGB, e com Cb e Cr¶

In [11]:
# Matplotlib figure sizing
plt.rcParams['figure.figsize'] = [9, 9]

O modelo RBG é um modelo de cor que, com o conhecimento limitado na altura da sua conceção, tenta simular os cones presentes no olho humano, representando as imagens com 3 canais: Vermelho, Verde, e Vermelho.

No entanto, como todos os canais contêm informação de luminância e cor, o modelo RGB apresenta uma elevada redundância no sinal.

O modelo de cor YCbCr extrai a luminância dos 3 canais de RGB para um único canal, $Y$, e toma em conta a sensibilidade do olho humano ao fazê-lo. Como o olho é mais sensível ao verde e vermelho, esta componente usa menos informação do canal azul. $$Y = 0.3R + 0.6G + 0.1B$$

O canal $Y$ é uma representação acromática da imagem original. Este canal não é comprimido pelo codec de JPEG, pois o olho humano é sensível à informação contida na porção acromática da imagem, como se pode verificar aqui.

Os restantes canais, $Cb$ e $Cr$, são canais de crominância, que representam as diferenças dos canais azul e vermelho da luminância, respetivamente. Como o olho humano é menos sensível a estas componentes, podemos reduzir o número de amostras - downsampling - sem alterar a perceção da imagem reconstruída de forma significativa.

In [12]:
colormodels(barn)
2022-03-25T20:43:51.584410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
2022-03-25T20:43:51.730411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

3. Downsampling e os seus efeitos sobre a imagem¶

Como apresentado acima, os canais de crominância podem ser resampled com menor detalhe, pois a uma distância de visualização normal, não existem perdas percetíveis de qualidade.

O codec JPEG permite rácios típicos de subsampling de 4:4:4 (sem subsampling), 4:2:2 , e 4:2:0. O rácio 4:2:2 reduz a resolução horizontal em ambos os canais Cb e Cr para metade, enquanto que o rácio 4:2:0 reduz a resolução horizontal e vertical para metade. Intuitivamente, podemos concluir que 4:2:0 resulta numa maior taxa de compressão ao custo de uma maior destrutividade, relativamente ao rácio 4:2:2.

De facto, as taxas de compressões relativas ao rácio 4:4:4 são:

  • 4:2:2: $\frac{4+4+4}{4+2+2} = 12\text{:}8 = 3\text{:}2$
  • 4:2:0: $\frac{4+4+4}{4+2+0} = 12\text{:}6 = 2\text{:}1$
In [13]:
subsampling(barn, ratio=(4,2,2))
2022-03-25T20:43:51.860412 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [14]:
subsampling(barn, ratio=(4,2,0))
2022-03-25T20:43:51.984409 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

4. Transformada de Cosseno Discreta (DCT)¶

A Transformada de Cosseno Discreta é uma transformada para um sinal de entrada discreto para uma soma de cossenos, guardando os coeeficientes das frequências resultantes. Esta transformada é usada pela potencialidade de representação das frequências espaciais mais altas com menos bits - quantização - pois o olho humano é menos sensível a variações nestes componentes da transformada.

4.1. DCT em canais inteiros¶

As transformadas dos canais inteiros apresentam coeficientes AC não negligenciáveis, isto porque existem transições inevitáveis quando o escopo da transformada abrange o canal inteiro. A compressão da DCT iria levar a perdas significativas da imagem original. Podemos restringir o escopo das transformadas para blocos mais pequenos, de modo a captar zonas da imagem mais suaves, nas quais os coeficientes AC sejam diminutos, e por conseguinte, mais dispensáveis.

In [15]:
DCT(barn, ratio=(4,2,0))
2022-03-25T20:43:52.113411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

4.2. DCT em blocos 8x8¶

Ao aplicar a transformada em blocos de 8x8, a probabilidade de apanhar transições abruptas de píxeis é reduzida drasticamente, pelo que o potencial de compressão da imagem aumenta. Isto porque, numa imagem suave, a DCT apresenta maior energia nas frequências baixas, o que faz com que a maioria da informação esteja contida num número reduzido de píxeis.

In [16]:
DCT(barn, ratio=(4,2,0), block=8)
2022-03-25T20:43:52.285410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

4.3. DCT em blocos 64x64¶

Aplicando a DCT em blocos de 64x64 consegue-se um melhor resultado do que num canal inteiro, mas resultaria num pior potencial de compressão do que blocos de 8x8 pois a probabilidade da existência de transições menos suaves em cada bloco aumenta, o que levaria a um maior número de frequências elevadas não negligenciáveis na recuperação da imagem original com relativa qualidade.

In [17]:
DCT(barn, ratio=(4,2,0), block=64)
2022-03-25T20:43:52.427411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

5. Quantização¶

A quantização afeta desproporcionalmente os coeficientes DC e AC de cada bloco da DCT, pois o canto superior esquerdo de cada bloco contém a informação mais importante para a reconstrução de uma imagem com detalhe e qualidade relativa - o coeficiente DC e os primeiros coeficientes AC. A esta discriminação dá-se o nome de quantização adaptativa.

A quantização consiste na divisão dos blocos da DCT por uma matriz, seguida de um arredondamento que inevitavelmente leva a destruição de dados de forma irrecuperável.

À medida que se reduz o fator de qualidade, os coeficientes resultantes ficam cada vez menores, como resultado do aumento dos valores da matriz de quantização. Isto leva a uma maior destrutividade de dados, e por conseguinte, perda de qualidade, que por outro lado, leva uma maior taxa de compressão final.

A imagem fica com áreas crescentes de coeficientes baixos, visível nas imagens abaixo pelas áreas negras cada vez maiores, que permite a representação destas com um número decrescente de bits.

Comparando com os resultados da DCT, e como consequência do referido acima, a quantização introduz um correlacionamento entre coeficientes adjacentes, o qual é potenciado posteriormente pela codificação diferencial, ainda apresentada neste trabalho, e por métodos de compressão como Huffman ou RLE, os quais estão fora do escopo deste projeto.

Fator de qualidade: 100

In [18]:
quantization(barn, qf=100)
2022-03-25T20:43:52.680411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Fator de qualidade: 75

In [19]:
quantization(barn, qf=75)
2022-03-25T20:43:52.888411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Fator de qualidade: 50

In [20]:
quantization(barn, qf=50)
2022-03-25T20:43:53.049411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Fator de qualidade: 25

In [21]:
quantization(barn, qf=25)
2022-03-25T20:43:53.208411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

Fator de qualidade: 10

In [22]:
quantization(barn, qf=10)
2022-03-25T20:43:53.361410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

6. Codificação DPCM¶

A codificação DPCM aplicada é uma codificação diferencial básica sobre os coeficientes DC da DCT, em que o coeficiente $c_i$ é codificado como a diferença entre este e o anterior: $d_i = c_i - c_{i-1}$.

Em imagens com transições suaves, estes valores são semelhantes - existe uma correlação elevada entre coeficientes adjacentes - pelo que as diferenças serão pequenas. Esta propriedade permite um estreitamento da gama de valores e uma menor variância destes, o que abre as portas para métodos de compressão entrópica.

Abaixo apresentamos a imagem antes e depois da codficação DPCM. É possível observer zonas do céu que ficam praticamente a negro, especialmente nos canais Cb e CR, e uma redução notável, ainda que menor, do valor de alguns coeficientes no centro dos canais.

In [23]:
DPCM(barn, ratio=(4,2,0), qf=75)
2022-03-25T20:43:53.613411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
2022-03-25T20:43:53.684411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

7. Codificação e descodificação end-to-end¶

Para todas as imagens observadas e com o decréscimo do fator de qualidade, os valores do Mean Square Error (MSE), e por conseguinte os valores da Root Mean Square Error (RMSE), aumentam, enquanto que o Signal-to-Noise Ratio (SNR) e o Peak Noise-to-Signal Ratio (PNSR) diminui - alterações indicativas de uma compressão destrutiva, tal como esperado.

No geral, com a diminiuição do fator de qualidade, as imagens do erro na reconstrução do canal $Y$ ficam mais intensas - o erro aumenta. Este erro tende a ser mais pronunciado nas zonas de transição abruptas, espalhando-se mais pela imagem com a redução da qualidade.

As imagens com gradientes suaves perdem resolução a partir do fator de qualidade 50, sendo visível linhas de transição nestas.

7.1. barn_mountains.bmp¶

Embora as métricas indiquem maior erro e ruído na reconstrução entre os fatores de qualidade 75 e 50, a imagem do erro no canal Y perde intensidade, o que nos indica que a compressão obtida deverá ter afetado mais os canais cromáticos.

Podemos ainda observar a partir da qualidade 50 o aparecimento de divisões de píxeis pretos e brancos formando linhas no céu na imagem do erro. Na imagem comprimida, estas linhas surgem nas transições anteriormente suaves do céu.

In [24]:
verboseMetrics(barn, qf=100, ratio=(4,2,0))
MSE: 20.550
RMSE: 4.533
SNR: 34.058 dB
PSNR: 35.003 dB
2022-03-25T20:43:53.921410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [25]:
verboseMetrics(barn, qf=75, ratio=(4,2,0))
MSE: 171.586
RMSE: 13.099
SNR: 24.841 dB
PSNR: 25.786 dB
2022-03-25T20:43:54.203411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [26]:
verboseMetrics(barn, qf=50, ratio=(4,2,0))
MSE: 282.302
RMSE: 16.802
SNR: 22.679 dB
PSNR: 23.624 dB
2022-03-25T20:43:54.486410 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [27]:
verboseMetrics(barn, qf=25, ratio=(4,2,0))
MSE: 422.080
RMSE: 20.545
SNR: 20.932 dB
PSNR: 21.877 dB
2022-03-25T20:43:54.790412 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [28]:
verboseMetrics(barn, qf=10, ratio=(4,2,0))
MSE: 740.352
RMSE: 27.209
SNR: 18.492 dB
PSNR: 19.436 dB
2022-03-25T20:43:55.068411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

7.2. peppers.bmp¶

Aqui, a imagem de diferenças entre os fatores de qualidade 75 e 50 ganham intensidade - existe uma maior diferença (absoluta) entre o canal $Y$ da imagem original e reconstruída. No entanto, parece que o erro baixa nos contornos dos pimentos no fator de qualidade 25, mas volta a subir com fator de qualidade 10, apresentando valores não negligenciáveis no canal inteiro.

In [29]:
verboseMetrics(peppers, qf=100, ratio=(4,2,0))
MSE: 12.649
RMSE: 3.557
SNR: 33.905 dB
PSNR: 37.110 dB
2022-03-25T20:43:55.483411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [30]:
verboseMetrics(peppers, qf=75, ratio=(4,2,0))
MSE: 70.840
RMSE: 8.417
SNR: 26.423 dB
PSNR: 29.628 dB
2022-03-25T20:43:55.893411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [31]:
verboseMetrics(peppers, qf=50, ratio=(4,2,0))
MSE: 106.063
RMSE: 10.299
SNR: 24.670 dB
PSNR: 27.875 dB
2022-03-25T20:43:56.281411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [32]:
verboseMetrics(peppers, qf=25, ratio=(4,2,0))
MSE: 162.495
RMSE: 12.747
SNR: 22.818 dB
PSNR: 26.022 dB
2022-03-25T20:43:56.698411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [33]:
verboseMetrics(peppers, qf=10, ratio=(4,2,0))
MSE: 341.954
RMSE: 18.492
SNR: 19.586 dB
PSNR: 22.791 dB
2022-03-25T20:43:57.173411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/

7.3. logo.bmp¶

Nesta imagem, o contorno visível na imagem de diferenças corresponde exatamente aos contornos do logótipo, pois são zonas de transição não suave de cores.

Os erros visíveis começam por ser um contorno leve mas grosso, que com a progressão do fator de qualidade vai ficando mais denso e mais restrito ao contorno real da imagem.

A partir do fator de qualidade 75, pode-se observar frequências fantasma (aliasing) no contorno da parte azul do logótipo, as quais ficam mais pronunciadas com o decréscimo do fator de qualidade.

A natureza da imagem - sem transições suaves, linhas muito bem definidas - resulta num mau desempenho do codec, devendo ser considerado um formato de imagem vetorial, como o SVG.

In [34]:
verboseMetrics(logo, qf=100, ratio=(4,2,0))
MSE: 9.283
RMSE: 3.047
SNR: 41.655 dB
PSNR: 38.454 dB
2022-03-25T20:43:57.456411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [35]:
verboseMetrics(logo, qf=75, ratio=(4,2,0))
MSE: 33.079
RMSE: 5.751
SNR: 36.137 dB
PSNR: 32.935 dB
2022-03-25T20:43:57.709411 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [36]:
verboseMetrics(logo, qf=50, ratio=(4,2,0))
MSE: 55.404
RMSE: 7.443
SNR: 33.897 dB
PSNR: 30.695 dB
2022-03-25T20:43:57.989934 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [37]:
verboseMetrics(logo, qf=25, ratio=(4,2,0))
MSE: 84.155
RMSE: 9.174
SNR: 32.081 dB
PSNR: 28.880 dB
2022-03-25T20:43:58.302969 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/
In [38]:
verboseMetrics(logo, qf=10, ratio=(4,2,0))
MSE: 182.662
RMSE: 13.515
SNR: 28.716 dB
PSNR: 25.514 dB
2022-03-25T20:43:58.553975 image/svg+xml Matplotlib v3.5.1, https://matplotlib.org/